'DistriLog
'Author: Anand Mukundan, www.dotnetindia.com

Imports System.Diagnostics
Imports System.Reflection
Imports System.Data.SqlClient
Imports System.Data.SqlServerCe
Imports System.Net
Imports System.Text
Imports System.ComponentModel

''' <summary>
''' Type of the Message
''' </summary>
''' <remarks> I have added a few extra types. Will seal out the types based on
''' feedback that I receive.</remarks>
Public Enum LogTypes
    [Error]
    Information
    Warning
    DebugInformation
    DeveloperTrace
End Enum

''' <summary>
''' This is the Local Logger. This handle writing to the local log as well as synchronize to the server log
''' </summary>
''' <remarks>This implements a Dispose as we hold the connection to the local database open for performance
''' reasons. This connection is closed in Dispose or Finalize only.</remarks>
Public Class LocalLogger
    Implements IDisposable

    'the local sdf database is in the same path as the DLL to log messages
    Private conn As SqlCeConnection = New SqlCeConnection("Data Source=LocalLog.sdf")
    'Save the list of IPs this machine has the first time and just use that
    Private ClientIP As String
    'Timer to launch the Synchronizer
    Private WithEvents SyncTimer As New System.Timers.Timer
    'The Client ID of be used if none is passed by the caller
    Private DefaultClientId As String
    'Finding out all the loaded assemblies is a time consuming task. So that list is cached here
    Private LoadedAssemblies As String = String.Empty
    'Environment Info Cache
    Private EnvironmentInfo As String = String.Empty

#Region " Async Logging Support and Method"
    Dim WithEvents BackgroundThread As BackgroundWorker
    ''' <summary>
    ''' Structure used to pass information to the Asynchronous worker thread
    ''' </summary>
    ''' <remarks></remarks>
    Structure AsyncParameters
        Dim Message As String
        Dim Logtype As LogTypes
        Dim ClientID As String
        Dim StackTrace As String
        Dim ExtendedInfo As String
    End Structure
    ''' <summary>
    ''' Fire and Forget Logger, which logs in a seperate background thread
    ''' </summary>
    ''' <param name="Message"></param>
    ''' <param name="LogType"></param>
    ''' <param name="StackTrace"></param>
    ''' <param name="ExtendedInfo"></param>
    ''' <param name="ClientId"></param>
    ''' <remarks></remarks>
    Public Sub LogAsync(ByVal Message As String, ByVal LogType As LogTypes, ByVal StackTrace As String, ByVal ExtendedInfo As String, Optional ByVal ClientId As String = "")
        If BackgroundThread Is Nothing Then
            BackgroundThread = New BackgroundWorker
        End If
        Dim Parameters As New AsyncParameters
        With Parameters
            .Message = Message
            .Logtype = LogType
            .ClientID = ClientId
            .StackTrace = StackTrace
            .ExtendedInfo = ExtendedInfo
        End With
        BackgroundThread.RunWorkerAsync(Parameters)
    End Sub
    ''' <summary>
    ''' The Actual Logging happens in this function when LogAsync is called
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Private Sub BackgroundThread_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundThread.DoWork
        Dim Parameters As AsyncParameters
        Parameters = e.Argument
        LogExtended(Parameters.Message, Parameters.Logtype, Parameters.StackTrace, Parameters.ExtendedInfo, "", Parameters.ClientID)
        BackgroundThread = Nothing
    End Sub

#End Region

    ''' <summary>
    ''' The simplest Log function
    ''' </summary>
    ''' <param name="Message"></param>
    ''' <param name="LogType"></param>
    ''' <param name="ClientId"></param>
    ''' <returns>Whether the logging was sucessful (0) or not (-1)</returns>
    ''' <remarks></remarks>
    Public Function Log(ByVal Message As String, ByVal LogType As LogTypes, Optional ByVal ClientId As String = "") As Integer

        Try
            Return LogExtended(Message, LogType, Nothing, Nothing, Nothing, ClientId)
        Catch ex As Exception
            'TODO: Handle Exceptions
            Log = -1
        End Try

    End Function

    ''' <summary>
    ''' Log an Error. Automatically sets type as Error and requires Exception Info and Stack Trace
    ''' </summary>
    ''' <param name="Message"></param>
    ''' <param name="StackTrace"></param>
    ''' <param name="ExceptionInfo"></param>
    ''' <param name="ExtendedInfo"></param>
    ''' <param name="ClientId"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function LogError(ByVal Message As String, ByVal StackTrace As String, ByVal ExceptionInfo As String, ByVal ExtendedInfo As String, Optional ByVal ClientId As String = "") As Integer
        Try
            Return LogExtended(Message, LogTypes.Error, StackTrace, ExtendedInfo, ExceptionInfo, ClientId)
        Catch ex As Exception
            'TODO: Handle Exceptions
            LogError = -1
        End Try
    End Function

    ''' <summary>
    ''' Log all Information. Can Provide any kind of custom information via ExtendedInfo. 
    ''' All Other Functions call this function internally
    ''' </summary>
    ''' <param name="Message"></param>
    ''' <param name="LogType"></param>
    ''' <param name="CallStack"></param>
    ''' <param name="ExtendedInfo"></param>
    ''' <param name="ExceptionInfo"></param>
    ''' <param name="ClientId"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function LogExtended(ByVal Message As String, _
        ByVal LogType As LogTypes, _
        ByVal CallStack As String, _
        ByVal ExtendedInfo As String, _
        ByVal ExceptionInfo As String, _
        Optional ByVal ClientId As String = "") As Integer

        Dim InsertCommand As New SqlCeCommand
        'Return Success
        LogExtended = 0
        If ClientId.Length = 0 Then
            ClientId = DefaultClientId
        End If
        Dim SQLFormat As String = "Insert into LocalLog values (@MessageId,@ClientID,@ClientIP,@Message,@StackTrace,@LogType,@InsertTime,@ExtendedInfo, @ExceptionInfo, @EnvironmentInfo, @TimeStamp)"
        Try
            InsertCommand.Connection = conn
            InsertCommand.CommandText = SQLFormat
    
            With InsertCommand.Parameters
                .Add("@MessageId", Guid.NewGuid.ToString)
                .Add("@ClientId", ClientId)
                .Add("@ClientIP", ClientIP)
                .Add("@Message", Message)
                If CallStack Is Nothing Then
                    .Add("@StackTrace", DBNull.Value)
                Else
                    .Add("@StackTrace", CallStack.ToString)
                End If
                .Add("@LogType", LogType)
                .Add("@InsertTime", DateTime.Now)
                .Add("@ExtendedInfo", IIf(ExtendedInfo Is Nothing, DBNull.Value, ExtendedInfo))
                .Add("@ExceptionInfo", IIf(ExceptionInfo Is Nothing, DBNull.Value, ExceptionInfo))
                .Add("@EnvironmentInfo", GetEnvironmentInfo)
                .Add("@TimeStamp", Now.Ticks)
            End With
            Dim RecordsAffected As Integer = InsertCommand.ExecuteScalar()
            If RecordsAffected = 0 Then
                LogExtended = -1
            End If
        Catch ex As Exception
            'TODO: Handle Exceptions
            LogExtended = -1
        End Try
    End Function

#Region " Constructor "

    Public Sub New()
        Dim clientHostName As String = Dns.GetHostName()
        Dim IPAddresses As IPAddress() = Dns.GetHostAddresses(clientHostName)
        Dim sb As New StringBuilder(1000)
        'Create a List of All IP Addresses assigned to this machine
        For Each Address As IPAddress In IPAddresses
            sb.Append(Address.ToString)
            sb.Append(";")
        Next
        ClientIP = sb.ToString
        'Since database can take only 500 Chars, restrict our information to 500 chars
        If ClientIP.Length > 500 Then
            ClientIP = ClientIP.Substring(0, 500)
        End If

        'Open Connection to Local Database
        'Since this is a local database and specific to this particular instance only, 
        'we can afford to keep it open thru lifetime. This gets closed during dispose or finalize only
        conn.Open()
        'Synchronize every minute
        SyncTimer.Interval = My.Settings.SyncTime
        SyncTimer.Enabled = True
        'If the app does not pass and Identifier we use the AssemblyName as an Identifier
        'We prepend the Default ID string to show that this was auto-generated
        DefaultClientId = "Default Id :" & Reflection.Assembly.GetCallingAssembly.GetName.ToString
        sb = Nothing
    End Sub
#End Region

#Region " Helper Functions "
    ''' <summary>
    ''' Gets the Environment Information from the different My objects and builds a string with the information
    ''' </summary>
    ''' <returns>Environment Information as a string</returns>
    ''' <remarks>If asked to cache, will cache the data in the EnvironmentInfo variable</remarks>
    Private Function GetEnvironmentInfo() As String
        If String.IsNullOrEmpty(EnvironmentInfo) Then
            Return EnvironmentInfo
        End If
        Dim sb As New StringBuilder(5000)
        BuildString(sb, "Machine Name : ", My.Computer.Name)
        BuildString(sb, "Total Memory (Physical): ", My.Computer.Info.TotalPhysicalMemory)
        BuildString(sb, "Total Memory (Virtual): ", My.Computer.Info.TotalVirtualMemory)
        BuildString(sb, "Free Memory (Physical): ", My.Computer.Info.AvailablePhysicalMemory)
        BuildString(sb, "Free Memory (Virtual): ", My.Computer.Info.AvailableVirtualMemory)
        BuildString(sb, "OS Version : ", My.Computer.Info.OSFullName & " " & My.Computer.Info.OSPlatform & " " & My.Computer.Info.OSVersion)
        BuildString(sb, "Application Assembly : ", My.Application.Info.AssemblyName)
        BuildString(sb, "Application Direcotry Path : ", My.Application.Info.DirectoryPath)
        BuildString(sb, "Working Set : ", My.Application.Info.WorkingSet)
        BuildString(sb, "Loaded Assemblies ", GetLoadedAssemblies())
        If My.Settings.CacheEnvironment Then
            EnvironmentInfo = sb.ToString
        End If
        Return sb.ToString
    End Function


    ''' <summary>
    ''' Helper Function to Get a list of Loaded Assemblies via Reflection
    ''' </summary>
    ''' <returns> A String with information on all Currently Loaded Assemblies</returns>
    ''' <remarks> Uses the CacheLoadedAssemblies Setting</remarks>
    Private Function GetLoadedAssemblies() As String
        'If there is no cache then load data via reflection, else return the cache
        If Not String.IsNullOrEmpty(LoadedAssemblies) Then
            Return LoadedAssemblies
        End If

        Dim LocalAssembly As Assembly
        Dim sb As New StringBuilder(3000)

        'Formatting constants- tab1 = 1 Tab, tab2 = 2 Tabs
        Const tab1 As String = vbTab
        Const tab2 As String = tab1 & vbTab

        sb.Append(vbCrLf)
        For Each LocalAssembly In My.Application.Info.LoadedAssemblies
            BuildString(sb, tab1 & "Assembly Name : ", LocalAssembly.FullName)
            BuildString(sb, tab2 & "Assembly Path : ", LocalAssembly.Location)
            BuildString(sb, tab2 & "Was Loaded from GAC :", LocalAssembly.GlobalAssemblyCache)
            BuildString(sb, tab2 & "Assembly CLR version : ", LocalAssembly.ImageRuntimeVersion)
        Next
        sb.Append(vbCrLf)
        'Cache this data only if the settings CacheLoadedAssemblies is true
        If My.Settings.CacheLoadedAssemblies Then
            LoadedAssemblies = sb.ToString
        Else
            'This should not be needed, just there for safety
            LoadedAssemblies = String.Empty
        End If
        Return sb.ToString
    End Function


    ''' <summary>
    ''' Helper Procedure to build Strings of "Name=Value" kind of data using a StringBuilder
    ''' </summary>
    ''' <param name="sBuilder">The StringBuilder to use</param>
    ''' <param name="Label">The Label Part of the Data</param>
    ''' <param name="Value">The Value Part of the Data</param>
    ''' <remarks>This Function Automatically appends a CrLf, but no other decorations. If there need to be spaces
    ''' or other decorations between the Label and Value those have to come from the source.</remarks>
    Private Sub BuildString(ByVal sBuilder As StringBuilder, ByVal Label As String, ByVal Value As String)
        If sBuilder Is Nothing OrElse String.IsNullOrEmpty(Label) OrElse String.IsNullOrEmpty(Value) Then
            Return
        End If
        With sBuilder
            .Append(Label)
            .Append(Value)
            .Append(vbCrLf)
        End With
    End Sub
#End Region

#Region " IDisposable Support and Finalizer "

    Private disposedValue As Boolean = False        ' To detect redundant calls

    ''' <summary>
    ''' Since we have open Connection, we need to close that in dispose
    ''' </summary>
    ''' <param name="disposing"></param>
    ''' <remarks>Dispose also does a final Synch of data with the central server</remarks>
    Protected Overridable Sub Dispose(ByVal disposing As Boolean)
        If Not Me.disposedValue Then
            If disposing Then
                SyncTimer.Enabled = False
                SyncTimer = Nothing
                'Do a   " Final Synchronization before ending
                SynchData()
                'Close the local connection
                conn.Close()

            End If

            ' TODO: free shared unmanaged resources
        End If
        Me.disposedValue = True
    End Sub

    ' This code added by Visual Basic to correctly implement the disposable pattern.
    Public Sub Dispose() Implements IDisposable.Dispose
        ' Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub

    ''' <summary>
    ''' If Dispose is not called, close the connection in the Finalizer
    ''' </summary>
    ''' <remarks></remarks>
    Protected Overrides Sub Finalize()
        SyncTimer.Enabled = False
        SyncTimer = Nothing

        'NOTE: Will not do a Data Synch on Finalize as the finalizer thread 
        'has a timeout for the finalizer, which can result in this thread
        'being aborted if the Data Synch takes too long

        'Close the local connection
        conn.Close()
        MyBase.Finalize()
    End Sub
#End Region


#Region " Synchronization Code "

    Public Sub SynchData()
        ' I am doing a manual synch as of now. This can move to MS Synch Services once it is out
        Dim ServerConnection As New SqlConnection(My.Settings.CentralLogConnectionString)
        Dim LocalCommand As New SqlCeCommand
        Dim ServerCommand As New SqlCommand
        Dim InsertStatement As String = _
        "INSERT INTO [CentralLog].[dbo].[CentralLog]([MessageID],[ClientID],[ClientIP],[Message],[StackTrace]" & _
        ",[MessageType],[ClientInsertDateTime],[ExtendedInformation],[ExceptionInfo],[ServerSyncDateTime],[ClientIPDuringSync],[EnvironmentInfo])" & _
        "VALUES" & _
        "(@MessageID,@ClientID, @ClientIP, @Message,@StackTrace,@MessageType,@ClientInsertDateTime, @ExtendedInformation,@ExceptionInfo,@ServerSyncDateTime,@ClientIPDuringSync,@EnvironmentInfo)"
        Dim LastUpdateTicks As String
        LocalCommand.Connection = conn
        LocalCommand.CommandType = CommandType.Text
        LocalCommand.CommandText = "Select ParameterValue from SystemParameters where ParameterName='LastSync'"
        LastUpdateTicks = LocalCommand.ExecuteScalar()
        LocalCommand.CommandText = "Update SystemParameters Set ParameterValue='" & Now.Ticks & "' where ParameterName='LastSync'"
        LocalCommand.ExecuteNonQuery()
        If LastUpdateTicks = 0 Then
            LastUpdateTicks = Now.Ticks
        End If

        LocalCommand.CommandText = "Select * from LocalLog where TimeStamp>'" & LastUpdateTicks & "'"
        Dim LogReader As SqlCeDataReader = LocalCommand.ExecuteReader

        ServerCommand.CommandType = CommandType.Text
        ServerCommand.CommandText = InsertStatement
        ServerCommand.Connection = ServerConnection
        ServerConnection.Open()
        While LogReader.Read
            With ServerCommand.Parameters
                .AddWithValue("@MessageID", LogReader.GetSqlGuid(0))
                .AddWithValue("@ClientId", LogReader.GetString(1))
                .AddWithValue("@ClientIP", LogReader.GetString(2))
                .AddWithValue("@Message", LogReader.GetString(3))
                If LogReader.IsDBNull(4) Then
                    .AddWithValue("@StackTrace", DBNull.Value)
                Else
                    .AddWithValue("@StackTrace", LogReader.GetString(4))
                End If

                .AddWithValue("@MessageType", LogReader.GetInt32(5))
                .AddWithValue("@ClientInsertDateTime", LogReader.GetDateTime(6))
                If LogReader.IsDBNull(7) Then
                    .AddWithValue("@ExtendedInformation", DBNull.Value)
                Else
                    .AddWithValue("@ExtendedInformation", LogReader.GetString(7))
                End If
                If LogReader.IsDBNull(8) Then
                    .AddWithValue("@ExceptionInfo", DBNull.Value)
                Else
                    .AddWithValue("@ExceptionInfo", LogReader.GetString(8))
                End If

                If LogReader.IsDBNull(9) Then
                    .AddWithValue("@EnvironmentInfo", DBNull.Value)
                Else
                    .AddWithValue("@EnvironmentInfo", LogReader.GetString(9))
                End If
                .AddWithValue("@ServerSyncDateTime", Now)
                .AddWithValue("@ClientIPDuringSync", ClientIP)
            End With
            ServerCommand.ExecuteNonQuery()
            ServerCommand.Parameters.Clear()
        End While
        
        ServerConnection.Close()
    End Sub

    Private Sub SyncTimer_Elapsed(ByVal sender As Object, ByVal e As System.Timers.ElapsedEventArgs) Handles SyncTimer.Elapsed
        SyncTimer.Enabled = False
        SynchData()
        SyncTimer.Enabled = True
    End Sub

#End Region
End Class
